{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 自然语言处理应用"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 概述\n",
    "\n",
    "情感分类是自然语言处理中文本分类问题的子集，属于自然语言处理最基础的应用。它是对带有感情色彩的主观性文本进行分析和推理的过程，即分析说话人的态度，是倾向正面还是反面。\n",
    "\n",
    "> 通常情况下，我们会把情感类别分为正面、反面和中性三类。虽然“面无表情”的评论也有不少；不过，大部分时候会只采用正面和反面的案例进行训练，下面这个数据集就是很好的例子。\n",
    "\n",
    "传统的文本主题分类问题的典型参考数据集为[20 Newsgroups](http://qwone.com/~jason/20Newsgroups/)，该数据集由20组新闻数据组成，包含约20000个新闻文档。\n",
    "其主题列表中有些类别的数据比较相似，例如comp.sys.ibm.pc.hardware和comp.sys.mac.hardware都是和电脑系统硬件相关的题目，相似度比较高。而有些主题类别的数据相对来说就毫无关联，例如misc.forsale和soc.religion.christian。\n",
    "\n",
    "就网络本身而言，文本主题分类的网络结构和情感分类的网络结构大致相似。在掌握了情感分类网络如何构造之后，很容易可以构造一个类似的网络，稍作调参即可用于文本主题分类任务。\n",
    "\n",
    "但在业务上下文侧，文本主题分类是分析文本讨论的客观内容，而情感分类是要从文本中得到它是否支持某种观点的信息。比如，“《阿甘正传》真是好看极了，影片主题明确，节奏流畅。”这句话，在文本主题分类是要将其归为类别为“电影”主题，而情感分类则要挖掘出这一影评的态度是正面还是负面。\n",
    "\n",
    "相对于传统的文本主题分类，情感分类较为简单，实用性也较强。常见的购物网站、电影网站都可以采集到相对高质量的数据集，也很容易给业务领域带来收益。例如，可以结合领域上下文，自动分析特定类型客户对当前产品的意见，可以分主题分用户类型对情感进行分析，以作针对性的处理，甚至基于此进一步推荐产品，提高转化率，带来更高的商业收益。\n",
    "\n",
    "特殊领域中，某些非极性词也充分表达了用户的情感倾向，比如下载使用APP时，“卡死了”、“下载太慢了”就表达了用户的负面情感倾向；股票领域中，“看涨”、“牛市”表达的就是用户的正面情感倾向。所以，本质上，我们希望模型能够在垂直领域中，挖掘出一些特殊的表达，作为极性词给情感分类系统使用：\n",
    "\n",
    "$垂直极性词 = 通用极性词 + 领域特有极性词$\n",
    "\n",
    "按照处理文本的粒度不同，情感分析可分为词语级、短语级、句子级、段落级以及篇章级等几个研究层次。这里以“段落级”为例，输入为一个段落，输出为影评是正面还是负面的信息。\n",
    "\n",
    "接下来，以IMDB影评情感分类为例来体验MindSpore在自然语言处理上的应用。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 整体流程\n",
    "\n",
    "1. 准备环节。\n",
    "2. 加载数据集，进行数据处理。\n",
    "3. 定义网络。\n",
    "4. 定义优化器和损失函数。\n",
    "5. 使用网络训练数据，生成模型。\n",
    "6. 得到模型之后，使用验证数据集，查看模型精度情况。\n",
    "\n",
    "> 本次体验流程支持CPU或GPU环境，Ascend环境暂不支持。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 准备环节\n",
    "\n",
    "### 下载数据集\n",
    "\n",
    "本次体验采用IMDB影评数据集作为实验数据。\n",
    "\n",
    "1. 下载IMDB影评数据集,数据集下载地址：<http://ai.stanford.edu/~amaas/data/sentiment/>。\n",
    "\n",
    "    以下是负面影评（Negative）和正面影评（Positive）的案例。\n",
    "\n",
    "| Review  | Label  | \n",
    "|:---|:---:|\n",
    "| \"Quitting\" may be as much about exiting a pre-ordained identity as about drug withdrawal. As a rural guy coming to Beijing, class and success must have struck this young artist face on as an appeal to separate from his roots and far surpass his peasant parents' acting success. Troubles arise, however, when the new man is too new, when it demands too big a departure from family, history, nature, and personal identity. The ensuing splits, and confusion between the imaginary and the real and the dissonance between the ordinary and the heroic are the stuff of a gut check on the one hand or a complete escape from self on the other.  |  Negative |  \n",
    "| This movie is amazing because the fact that the real people portray themselves and their real life experience and do such a good job it's like they're almost living the past over again. Jia Hongsheng plays himself an actor who quit everything except music and drugs struggling with depression and searching for the meaning of life while being angry at everyone especially the people who care for him most.  | Positive  |\n",
    "    \n",
    "&emsp;&emsp;将下载好的数据集解压并放在当前工作目录下。\n",
    "\n",
    "\n",
    "2. 下载GloVe文件\n",
    "    下载并解压GloVe文件到当前工作目录下，修改解压后的目录名为`glove`，并在所有Glove文件开头处添加如下所示新的一行，意思是总共读取400000个单词，每个单词用300纬度的词向量表示。\n",
    "\n",
    "    ```\n",
    "    400000 300\n",
    "    ```\n",
    "\n",
    "    GloVe文件下载地址：<http://nlp.stanford.edu/data/glove.6B.zip>\n",
    "\n",
    "\n",
    "3. 在当前工作目录创建名为`preprocess`的空目录，该目录将用于存储在数据集预处理操作中IMDB数据集转换为MindRecord格式后的文件。\n",
    "\n",
    "    此时当前工作目录结构如下所示。\n",
    "    \n",
    "    ```shell\n",
    "    $ tree -L 2 lstm\n",
    "    lstm\n",
    "    ├── aclImdb\n",
    "    │   ├── imdbEr.txt\n",
    "    │   ├── imdb.vocab\n",
    "    │   ├── README\n",
    "    │   ├── test\n",
    "    │   └── train\n",
    "    ├── glove\n",
    "    │   ├── glove.6B.100d.txt\n",
    "    │   ├── glove.6B.200d.txt\n",
    "    │   ├── glove.6B.300d.txt\n",
    "    │   └── glove.6B.50d.txt\n",
    "    └── preprocess\n",
    "    ```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 确定评价标准\n",
    "\n",
    "作为典型的分类问题，情感分类的评价标准可以比照普通的分类问题处理。常见的精度（Accuracy）、精准度（Precision）、召回率（Recall）和F_beta分数都可以作为参考。\n",
    "\n",
    "$精度（Accuracy）= 分类正确的样本数目 / 总样本数目$\n",
    "\n",
    "$精准度（Precision）= 真阳性样本数目 / 所有预测类别为阳性的样本数目$\n",
    "\n",
    "$召回率（Recall）= 真阳性样本数目 / 所有真实类别为阳性的样本数目$ \n",
    "\n",
    "$F1分数 = (2 * Precision * Recall) / (Precision + Recall)$\n",
    "\n",
    "在IMDB这个数据集中，正负样本数差别不大，可以简单地用精度（accuracy）作为分类器的衡量标准。\n",
    "\n",
    "### 确定网络\n",
    "\n",
    "我们使用基于LSTM构建的SentimentNet网络进行自然语言处理。\n",
    "\n",
    "> LSTM（Long short-term memory，长短期记忆）网络是一种时间循环神经网络，适合于处理和预测时间序列中间隔和延迟非常长的重要事件。\n",
    "> 本次体验面向GPU或CPU硬件平台。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 配置运行信息"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. 使用`parser`模块传入运行必要的信息。\n",
    "    \n",
    "    - `preprocess`：是否预处理数据集，默认为否。\n",
    "    - `aclimdb_path`：数据集存放路径。\n",
    "    - `glove_path`：GloVe文件存放路径。\n",
    "    - `preprocess_path`：预处理数据集的结果文件夹。\n",
    "    - `ckpt_path`：CheckPoint文件路径。\n",
    "    - `pre_trained`：预加载CheckPoint文件。\n",
    "    - `device_target`：指定GPU或CPU环境。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "import argparse\n",
    "\n",
    "\n",
    "parser = argparse.ArgumentParser(description='MindSpore LSTM Example')\n",
    "parser.add_argument('--preprocess', type=str, default='false', choices=['true', 'false'],\n",
    "                    help='whether to preprocess data.')\n",
    "parser.add_argument('--aclimdb_path', type=str, default=\"./aclImdb\",\n",
    "                    help='path where the dataset is stored.')\n",
    "parser.add_argument('--glove_path', type=str, default=\"./glove\",\n",
    "                    help='path where the GloVe is stored.')\n",
    "parser.add_argument('--preprocess_path', type=str, default=\"./preprocess\",\n",
    "                    help='path where the pre-process data is stored.')\n",
    "parser.add_argument('--ckpt_path', type=str, default=\"./\",\n",
    "                    help='the path to save the checkpoint file.')\n",
    "parser.add_argument('--pre_trained', type=str, default=None,\n",
    "                    help='the pretrained checkpoint file path.')\n",
    "parser.add_argument('--device_target', type=str, default=\"GPU\", choices=['GPU', 'CPU'],\n",
    "                    help='the target device to run, support \"GPU\", \"CPU\". Default: \"GPU\".')\n",
    "args = parser.parse_args(['--device_target', 'GPU', '--preprocess', 'true'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2. 进行训练前，需要配置必要的信息，包括环境信息、执行的模式、后端信息及硬件信息。 \n",
    "    \n",
    "> 详细的接口配置信息，请参见MindSpore官网`context.set_context`API接口说明。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "from mindspore import context\n",
    "\n",
    "\n",
    "context.set_context(\n",
    "        mode=context.GRAPH_MODE,\n",
    "        save_graphs=False,\n",
    "        device_target=args.device_target)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 配置SentimentNet网络参数\n",
    "\n",
    "在以下一段代码中配置基于LSTM构建的SentimentNet网络所需相关参数。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "from easydict import EasyDict as edict\n",
    "\n",
    "\n",
    "# LSTM CONFIG\n",
    "lstm_cfg = edict({\n",
    "    'num_classes': 2,\n",
    "    'learning_rate': 0.1,\n",
    "    'momentum': 0.9,\n",
    "    'num_epochs': 10,\n",
    "    'batch_size': 64,\n",
    "    'embed_size': 300,\n",
    "    'num_hiddens': 100,\n",
    "    'num_layers': 2,\n",
    "    'bidirectional': True,\n",
    "    'save_checkpoint_steps': 390,\n",
    "    'keep_checkpoint_max': 10\n",
    "})\n",
    "\n",
    "cfg = lstm_cfg"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 数据处理\n",
    "\n",
    "## 预处理数据集"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. 定义`ImdbParser`类解析文本数据集，包括编码、分词、对齐、处理GloVe原始数据，使之能够适应网络结构。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "from itertools import chain\n",
    "import numpy as np\n",
    "import gensim\n",
    "\n",
    "\n",
    "class ImdbParser():\n",
    "    \"\"\"\n",
    "    parse aclImdb data to features and labels.\n",
    "    sentence->tokenized->encoded->padding->features\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, imdb_path, glove_path, embed_size=300):\n",
    "        self.__segs = ['train', 'test']\n",
    "        self.__label_dic = {'pos': 1, 'neg': 0}\n",
    "        self.__imdb_path = imdb_path\n",
    "        self.__glove_dim = embed_size\n",
    "        self.__glove_file = os.path.join(glove_path, 'glove.6B.' + str(self.__glove_dim) + 'd.txt')\n",
    "\n",
    "        # properties\n",
    "        self.__imdb_datas = {}\n",
    "        self.__features = {}\n",
    "        self.__labels = {}\n",
    "        self.__vacab = {}\n",
    "        self.__word2idx = {}\n",
    "        self.__weight_np = {}\n",
    "        self.__wvmodel = None\n",
    "\n",
    "    def parse(self):\n",
    "        \"\"\"\n",
    "        parse imdb data to memory\n",
    "        \"\"\"\n",
    "        self.__wvmodel = gensim.models.KeyedVectors.load_word2vec_format(self.__glove_file)\n",
    "\n",
    "        for seg in self.__segs:\n",
    "            self.__parse_imdb_datas(seg)\n",
    "            self.__parse_features_and_labels(seg)\n",
    "            self.__gen_weight_np(seg)\n",
    "\n",
    "    def __parse_imdb_datas(self, seg):\n",
    "        \"\"\"\n",
    "        load data from txt\n",
    "        \"\"\"\n",
    "        data_lists = []\n",
    "        for label_name, label_id in self.__label_dic.items():\n",
    "            sentence_dir = os.path.join(self.__imdb_path, seg, label_name)\n",
    "            for file in os.listdir(sentence_dir):\n",
    "                with open(os.path.join(sentence_dir, file), mode='r', encoding='utf8') as f:\n",
    "                    sentence = f.read().replace('\\n', '')\n",
    "                    data_lists.append([sentence, label_id])\n",
    "        self.__imdb_datas[seg] = data_lists\n",
    "\n",
    "    def __parse_features_and_labels(self, seg):\n",
    "        \"\"\"\n",
    "        parse features and labels\n",
    "        \"\"\"\n",
    "        features = []\n",
    "        labels = []\n",
    "        for sentence, label in self.__imdb_datas[seg]:\n",
    "            features.append(sentence)\n",
    "            labels.append(label)\n",
    "\n",
    "        self.__features[seg] = features\n",
    "        self.__labels[seg] = labels\n",
    "\n",
    "        # update feature to tokenized\n",
    "        self.__updata_features_to_tokenized(seg)\n",
    "        # parse vacab\n",
    "        self.__parse_vacab(seg)\n",
    "        # encode feature\n",
    "        self.__encode_features(seg)\n",
    "        # padding feature\n",
    "        self.__padding_features(seg)\n",
    "\n",
    "    def __updata_features_to_tokenized(self, seg):\n",
    "        tokenized_features = []\n",
    "        for sentence in self.__features[seg]:\n",
    "            tokenized_sentence = [word.lower() for word in sentence.split(\" \")]\n",
    "            tokenized_features.append(tokenized_sentence)\n",
    "        self.__features[seg] = tokenized_features\n",
    "\n",
    "    def __parse_vacab(self, seg):\n",
    "        # vocab\n",
    "        tokenized_features = self.__features[seg]\n",
    "        vocab = set(chain(*tokenized_features))\n",
    "        self.__vacab[seg] = vocab\n",
    "\n",
    "        # word_to_idx: {'hello': 1, 'world':111, ... '<unk>': 0}\n",
    "        word_to_idx = {word: i + 1 for i, word in enumerate(vocab)}\n",
    "        word_to_idx['<unk>'] = 0\n",
    "        self.__word2idx[seg] = word_to_idx\n",
    "\n",
    "    def __encode_features(self, seg):\n",
    "        \"\"\" encode word to index \"\"\"\n",
    "        word_to_idx = self.__word2idx['train']\n",
    "        encoded_features = []\n",
    "        for tokenized_sentence in self.__features[seg]:\n",
    "            encoded_sentence = []\n",
    "            for word in tokenized_sentence:\n",
    "                encoded_sentence.append(word_to_idx.get(word, 0))\n",
    "            encoded_features.append(encoded_sentence)\n",
    "        self.__features[seg] = encoded_features\n",
    "\n",
    "    def __padding_features(self, seg, maxlen=500, pad=0):\n",
    "        \"\"\" pad all features to the same length \"\"\"\n",
    "        padded_features = []\n",
    "        for feature in self.__features[seg]:\n",
    "            if len(feature) >= maxlen:\n",
    "                padded_feature = feature[:maxlen]\n",
    "            else:\n",
    "                padded_feature = feature\n",
    "                while len(padded_feature) < maxlen:\n",
    "                    padded_feature.append(pad)\n",
    "            padded_features.append(padded_feature)\n",
    "        self.__features[seg] = padded_features\n",
    "\n",
    "    def __gen_weight_np(self, seg):\n",
    "        \"\"\"\n",
    "        generate weight by gensim\n",
    "        \"\"\"\n",
    "        weight_np = np.zeros((len(self.__word2idx[seg]), self.__glove_dim), dtype=np.float32)\n",
    "        for word, idx in self.__word2idx[seg].items():\n",
    "            if word not in self.__wvmodel:\n",
    "                continue\n",
    "            word_vector = self.__wvmodel.get_vector(word)\n",
    "            weight_np[idx, :] = word_vector\n",
    "\n",
    "        self.__weight_np[seg] = weight_np\n",
    "\n",
    "    def get_datas(self, seg):\n",
    "        \"\"\"\n",
    "        return features, labels, and weight\n",
    "        \"\"\"\n",
    "        features = np.array(self.__features[seg]).astype(np.int32)\n",
    "        labels = np.array(self.__labels[seg]).astype(np.int32)\n",
    "        weight = np.array(self.__weight_np[seg])\n",
    "        return features, labels, weight"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2. 定义`convert_to_mindrecord`函数将数据集格式转换为MindRecord格式，便于MindSpore读取。\n",
    "\n",
    "    函数`_convert_to_mindrecord`中`weight.txt`为数据预处理后自动生成的weight参数信息文件。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import numpy as np\n",
    "from mindspore.mindrecord import FileWriter\n",
    "\n",
    "\n",
    "def _convert_to_mindrecord(data_home, features, labels, weight_np=None, training=True):\n",
    "    \"\"\"\n",
    "    convert imdb dataset to mindrecoed dataset\n",
    "    \"\"\"\n",
    "    if weight_np is not None:\n",
    "        np.savetxt(os.path.join(data_home, 'weight.txt'), weight_np)\n",
    "\n",
    "    # write mindrecord\n",
    "    schema_json = {\"id\": {\"type\": \"int32\"},\n",
    "                   \"label\": {\"type\": \"int32\"},\n",
    "                   \"feature\": {\"type\": \"int32\", \"shape\": [-1]}}\n",
    "\n",
    "    data_dir = os.path.join(data_home, \"aclImdb_train.mindrecord\")\n",
    "    if not training:\n",
    "        data_dir = os.path.join(data_home, \"aclImdb_test.mindrecord\")\n",
    "\n",
    "    def get_imdb_data(features, labels):\n",
    "        data_list = []\n",
    "        for i, (label, feature) in enumerate(zip(labels, features)):\n",
    "            data_json = {\"id\": i,\n",
    "                         \"label\": int(label),\n",
    "                         \"feature\": feature.reshape(-1)}\n",
    "            data_list.append(data_json)\n",
    "        return data_list\n",
    "\n",
    "    writer = FileWriter(data_dir, shard_num=4)\n",
    "    data = get_imdb_data(features, labels)\n",
    "    writer.add_schema(schema_json, \"nlp_schema\")\n",
    "    writer.add_index([\"id\", \"label\"])\n",
    "    writer.write_raw_data(data)\n",
    "    writer.commit()\n",
    "\n",
    "\n",
    "def convert_to_mindrecord(embed_size, aclimdb_path, preprocess_path, glove_path):\n",
    "    \"\"\"\n",
    "    convert imdb dataset to mindrecoed dataset\n",
    "    \"\"\"\n",
    "    parser = ImdbParser(aclimdb_path, glove_path, embed_size)\n",
    "    parser.parse()\n",
    "\n",
    "    if not os.path.exists(preprocess_path):\n",
    "        print(f\"preprocess path {preprocess_path} is not exist\")\n",
    "        os.makedirs(preprocess_path)\n",
    "\n",
    "    train_features, train_labels, train_weight_np = parser.get_datas('train')\n",
    "    _convert_to_mindrecord(preprocess_path, train_features, train_labels, train_weight_np)\n",
    "\n",
    "    test_features, test_labels, _ = parser.get_datas('test')\n",
    "    _convert_to_mindrecord(preprocess_path, test_features, test_labels, training=False)\n",
    "    "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "3. 调用`convert_to_mindrecord`函数执行数据集预处理，此处用时约3分钟。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "============== Starting Data Pre-processing ==============\n",
      "======================= Successful =======================\n"
     ]
    }
   ],
   "source": [
    "if args.preprocess == \"true\":\n",
    "    print(\"============== Starting Data Pre-processing ==============\")\n",
    "    convert_to_mindrecord(cfg.embed_size, args.aclimdb_path, args.preprocess_path, args.glove_path)\n",
    "    print(\"======================= Successful =======================\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "&nbsp;&nbsp;&nbsp;&nbsp;转换成功后会在`preprocess`目录下生成MindRecord文件，通常该操作在数据集不变的情况下，无需每次训练都执行，此时`preprocess`文件目录如下所示：\n",
    "\n",
    "```shell\n",
    " $ tree preprocess\n",
    " ├── aclImdb_test.mindrecord0\n",
    " ├── aclImdb_test.mindrecord0.db\n",
    " ├── aclImdb_test.mindrecord1\n",
    " ├── aclImdb_test.mindrecord1.db\n",
    " ├── aclImdb_test.mindrecord2\n",
    " ├── aclImdb_test.mindrecord2.db\n",
    " ├── aclImdb_test.mindrecord3\n",
    " ├── aclImdb_test.mindrecord3.db\n",
    " ├── aclImdb_train.mindrecord0\n",
    " ├── aclImdb_train.mindrecord0.db\n",
    " ├── aclImdb_train.mindrecord1\n",
    " ├── aclImdb_train.mindrecord1.db\n",
    " ├── aclImdb_train.mindrecord2\n",
    " ├── aclImdb_train.mindrecord2.db\n",
    " ├── aclImdb_train.mindrecord3\n",
    " ├── aclImdb_train.mindrecord3.db\n",
    " └── weight.txt\n",
    "```\n",
    "\n",
    "- 以上各文件中：\n",
    "    - 名称包含`aclImdb_train.mindrecord`的为转换后的MindRecord格式的训练数据集。\n",
    "    - 名称包含`aclImdb_test.mindrecord`的为转换后的MindRecord格式的测试数据集。\n",
    "    - `weight.txt`为预处理后自动生成的weight参数信息文件。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "4. 定义创建数据集函数`lstm_create_dataset`，创建训练集`ds_train`。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import mindspore.dataset as ds\n",
    "\n",
    "\n",
    "def lstm_create_dataset(data_home, batch_size, repeat_num=1, training=True):\n",
    "    \"\"\"Data operations.\"\"\"\n",
    "    ds.config.set_seed(1)\n",
    "    data_dir = os.path.join(data_home, \"aclImdb_train.mindrecord0\")\n",
    "    if not training:\n",
    "        data_dir = os.path.join(data_home, \"aclImdb_test.mindrecord0\")\n",
    "\n",
    "    data_set = ds.MindDataset(data_dir, columns_list=[\"feature\", \"label\"], num_parallel_workers=4)\n",
    "\n",
    "    # apply map operations on images\n",
    "    data_set = data_set.shuffle(buffer_size=data_set.get_dataset_size())\n",
    "    data_set = data_set.batch(batch_size=batch_size, drop_remainder=True)\n",
    "    data_set = data_set.repeat(count=repeat_num)\n",
    "\n",
    "    return data_set\n",
    "\n",
    "ds_train = lstm_create_dataset(args.preprocess_path, cfg.batch_size)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "5. 通过`create_dict_iterator`方法创建字典迭代器，读取已创建的数据集`ds_train`中的数据。\n",
    "\n",
    "    运行以下一段代码，读取第1个`batch`中的`label`数据列表，和第1个`batch`中第1个元素的`feature`数据。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "The first batch contains label below:\n",
      "[0 0 1 1 1 0 0 0 0 0 0 1 0 0 1 0 1 1 0 0 0 0 1 1 1 0 1 1 1 0 0 1 0 0 1 0 1\n",
      " 0 0 0 0 1 0 0 1 1 1 0 0 0 1 1 1 1 0 1 0 0 1 1 0 1 1 0]\n",
      "\n",
      "The feature of the first item in the first batch is below vector:\n",
      "[210974 227370 167874 221440 205821 250308  57410 167874 157597 211314\n",
      " 104140 154424 238018 167874 216357  23869 209921 187724 131973 144940\n",
      " 177558 221440 205821 119691 149127 137330 212709 117415  61509  42345\n",
      " 166849 155531 219231  64473 210974 103293 225985 181047  41304 210974\n",
      " 132905  33755  96216   8987 210974 195260 117816  15665 241057   8987\n",
      "  93501 155531 118935 110275 101659 181047 226216 133895 114115   6596\n",
      " 189694 210974  56753   3426  29344 103100 131973  46391  25351  35080\n",
      "  27231  69404 190304 212709 117415 157277 167874 210974 109102  92239\n",
      " 101085 123273  64473 117415 176947  27231 168206 219146 167874 210974\n",
      " 227370  18539 155531 219231  64473 210974 155781  93577 192315 157597\n",
      " 213189  66091 216583 100381 158491 181047  15368 221440  15353 137110\n",
      " 190640 197076 150070 117415 216583  60701 227370 238232 117593 210974\n",
      " 141131 167874  59907 238018 184247 156120  68959 117415 149618 167874\n",
      " 110810 216614  80083  10164 238272 245070 213136 205012  53662 199932\n",
      " 208769 153768 225055  86111 156120  55862 199932  25351 243753 156120\n",
      " 232746 239534 210974   1793 245184 210005 232746   8987  25351 239106\n",
      "  23869  89702 232746 175897 221440 202670 181047 117415 169623 206889\n",
      " 236706 155531   8803 208769  36551      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0\n",
      "      0      0      0      0      0      0      0      0      0      0]\n"
     ]
    }
   ],
   "source": [
    "iterator = ds_train.create_dict_iterator().get_next()\n",
    "first_batch_label = iterator[\"label\"].asnumpy()\n",
    "first_batch_first_feature = iterator[\"feature\"].asnumpy()[0]\n",
    "print(f\"The first batch contains label below:\\n{first_batch_label}\\n\")\n",
    "print(f\"The feature of the first item in the first batch is below vector:\\n{first_batch_first_feature}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义网络\n",
    "\n",
    "1. 导入初始化网络所需模块。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "import math\n",
    "\n",
    "import numpy as np\n",
    "\n",
    "from mindspore import Tensor, nn, context, Parameter, ParameterTuple\n",
    "from mindspore.common.initializer import initializer\n",
    "from mindspore.ops import operations as P"
   ]
  },
  {
   "cell_type": "markdown",
   "source": [
    "2. 定义需要单层LSTM小算子堆叠的设备类型。"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "STACK_LSTM_DEVICE = [\"CPU\"]"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%%\n"
    }
   }
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "3. 定义`lstm_default_state`函数来初始化网络参数及网络状态。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Initialize short-term memory (h) and long-term memory (c) to 0\n",
    "def lstm_default_state(batch_size, hidden_size, num_layers, bidirectional):\n",
    "    \"\"\"init default input.\"\"\"\n",
    "    num_directions = 2 if bidirectional else 1\n",
    "    h = Tensor(np.zeros((num_layers * num_directions, batch_size, hidden_size)).astype(np.float32))\n",
    "    c = Tensor(np.zeros((num_layers * num_directions, batch_size, hidden_size)).astype(np.float32))\n",
    "    return h, c"
   ]
  },
  {
   "cell_type": "markdown",
   "source": [
    "4. 定义`stack_lstm_default_state`函数来初始化小算子堆叠需要的初始化网络参数及网络状态。"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "def stack_lstm_default_state(batch_size, hidden_size, num_layers, bidirectional):\n",
    "    \"\"\"init default input.\"\"\"\n",
    "    num_directions = 2 if bidirectional else 1\n",
    "\n",
    "    h_list = c_list = []\n",
    "    for _ in range(num_layers):\n",
    "        h_list.append(Tensor(np.zeros((num_directions, batch_size, hidden_size)).astype(np.float32)))\n",
    "        c_list.append(Tensor(np.zeros((num_directions, batch_size, hidden_size)).astype(np.float32)))\n",
    "    h, c = tuple(h_list), tuple(c_list)\n",
    "    return h, c\n"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%%\n"
    }
   }
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "5. 针对CPU场景，自定义单层LSTM小算子堆叠，来实现多层LSTM大算子功能。、"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "class StackLSTM(nn.Cell):\n",
    "    \"\"\"\n",
    "    Stack multi-layers LSTM together.\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self,\n",
    "                 input_size,\n",
    "                 hidden_size,\n",
    "                 num_layers=1,\n",
    "                 has_bias=True,\n",
    "                 batch_first=False,\n",
    "                 dropout=0.0,\n",
    "                 bidirectional=False):\n",
    "        super(StackLSTM, self).__init__()\n",
    "        self.num_layers = num_layers\n",
    "        self.batch_first = batch_first\n",
    "        self.transpose = P.Transpose()\n",
    "\n",
    "        # direction number\n",
    "        num_directions = 2 if bidirectional else 1\n",
    "\n",
    "        # input_size list\n",
    "        input_size_list = [input_size]\n",
    "        for i in range(num_layers - 1):\n",
    "            input_size_list.append(hidden_size * num_directions)\n",
    "\n",
    "        # layers\n",
    "        layers = []\n",
    "        for i in range(num_layers):\n",
    "            layers.append(nn.LSTMCell(input_size=input_size_list[i],\n",
    "                                      hidden_size=hidden_size,\n",
    "                                      has_bias=has_bias,\n",
    "                                      batch_first=batch_first,\n",
    "                                      bidirectional=bidirectional,\n",
    "                                      dropout=dropout))\n",
    "\n",
    "        # weights\n",
    "        weights = []\n",
    "        for i in range(num_layers):\n",
    "            # weight size\n",
    "            weight_size = (input_size_list[i] + hidden_size) * num_directions * hidden_size * 4\n",
    "            if has_bias:\n",
    "                bias_size = num_directions * hidden_size * 4\n",
    "                weight_size = weight_size + bias_size\n",
    "\n",
    "            # numpy weight\n",
    "            stdv = 1 / math.sqrt(hidden_size)\n",
    "            w_np = np.random.uniform(-stdv, stdv, (weight_size, 1, 1)).astype(np.float32)\n",
    "\n",
    "            # lstm weight\n",
    "            weights.append(Parameter(initializer(Tensor(w_np), w_np.shape), name=\"weight\" + str(i)))\n",
    "\n",
    "        #\n",
    "        self.lstms = layers\n",
    "        self.weight = ParameterTuple(tuple(weights))\n",
    "\n",
    "    def construct(self, x, hx):\n",
    "        \"\"\"construct\"\"\"\n",
    "        if self.batch_first:\n",
    "            x = self.transpose(x, (1, 0, 2))\n",
    "        # stack lstm\n",
    "        h, c = hx\n",
    "        hn = cn = None\n",
    "        for i in range(self.num_layers):\n",
    "            x, hn, cn, _, _ = self.lstms[i](x, h[i], c[i], self.weight[i])\n",
    "        if self.batch_first:\n",
    "            x = self.transpose(x, (1, 0, 2))\n",
    "        return x, (hn, cn)"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%%\n"
    }
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "6. 使用`Cell`方法，定义网络结构（`SentimentNet`网络）。"
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%% md\n"
    }
   }
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "class SentimentNet(nn.Cell):\n",
    "    \"\"\"Sentiment network structure.\"\"\"\n",
    "\n",
    "    def __init__(self,\n",
    "                 vocab_size,\n",
    "                 embed_size,\n",
    "                 num_hiddens,\n",
    "                 num_layers,\n",
    "                 bidirectional,\n",
    "                 num_classes,\n",
    "                 weight,\n",
    "                 batch_size):\n",
    "        super(SentimentNet, self).__init__()\n",
    "        # Mapp words to vectors\n",
    "        self.embedding = nn.Embedding(vocab_size,\n",
    "                                      embed_size,\n",
    "                                      embedding_table=weight)\n",
    "        self.embedding.embedding_table.requires_grad = False\n",
    "        self.trans = P.Transpose()\n",
    "        self.perm = (1, 0, 2)\n",
    "\n",
    "        if context.get_context(\"device_target\") in STACK_LSTM_DEVICE:\n",
    "            # stack lstm by user\n",
    "            self.encoder = StackLSTM(input_size=embed_size,\n",
    "                                     hidden_size=num_hiddens,\n",
    "                                     num_layers=num_layers,\n",
    "                                     has_bias=True,\n",
    "                                     bidirectional=bidirectional,\n",
    "                                     dropout=0.0)\n",
    "            self.h, self.c = stack_lstm_default_state(batch_size, num_hiddens, num_layers, bidirectional)\n",
    "        else:\n",
    "            # standard lstm\n",
    "            self.encoder = nn.LSTM(input_size=embed_size,\n",
    "                                   hidden_size=num_hiddens,\n",
    "                                   num_layers=num_layers,\n",
    "                                   has_bias=True,\n",
    "                                   bidirectional=bidirectional,\n",
    "                                   dropout=0.0)\n",
    "            self.h, self.c = lstm_default_state(batch_size, num_hiddens, num_layers, bidirectional)\n",
    "\n",
    "        self.concat = P.Concat(1)\n",
    "        if bidirectional:\n",
    "            self.decoder = nn.Dense(num_hiddens * 4, num_classes)\n",
    "        else:\n",
    "            self.decoder = nn.Dense(num_hiddens * 2, num_classes)\n",
    "\n",
    "    def construct(self, inputs):\n",
    "        # input：(64,500,300)\n",
    "        embeddings = self.embedding(inputs)\n",
    "        embeddings = self.trans(embeddings, self.perm)\n",
    "        output, _ = self.encoder(embeddings, (self.h, self.c))\n",
    "        # states[i] size(64,200)  -> encoding.size(64,400)\n",
    "        encoding = self.concat((output[0], output[499]))\n",
    "        outputs = self.decoder(encoding)\n",
    "        return outputs"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "7. 实例化`SentimentNet`，创建网络，此步骤用时约1分钟。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "embedding_table = np.loadtxt(os.path.join(args.preprocess_path, \"weight.txt\")).astype(np.float32)\n",
    "network = SentimentNet(vocab_size=embedding_table.shape[0],\n",
    "                       embed_size=cfg.embed_size,\n",
    "                       num_hiddens=cfg.num_hiddens,\n",
    "                       num_layers=cfg.num_layers,\n",
    "                       bidirectional=cfg.bidirectional,\n",
    "                       num_classes=cfg.num_classes,\n",
    "                       weight=Tensor(embedding_table),\n",
    "                       batch_size=cfg.batch_size)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义优化器及损失函数\n",
    "\n",
    "运行以下一段代码，创建优化器和损失函数模型。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "from mindspore import nn\n",
    "\n",
    "\n",
    "loss = nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean')\n",
    "opt = nn.Momentum(network.trainable_params(), cfg.learning_rate, cfg.momentum)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 训练并保存模型\n",
    "\n",
    "加载训练数据集（`ds_train`）并配置好`CheckPoint`生成信息，然后使用`model.train`接口，进行模型训练，此步骤用时约7分钟。根据输出可以看到loss值随着训练逐步降低，最后达到0.262左右。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "============== Starting Training ==============\n",
      "epoch: 1 step: 1, loss is 0.6938\n",
      "epoch: 1 step: 2, loss is 0.6922\n",
      "epoch: 1 step: 3, loss is 0.6917\n",
      "epoch: 1 step: 4, loss is 0.6952\n",
      "epoch: 1 step: 5, loss is 0.6868\n",
      "epoch: 1 step: 6, loss is 0.6982\n",
      "epoch: 1 step: 7, loss is 0.6856\n",
      "epoch: 1 step: 8, loss is 0.6819\n",
      "epoch: 1 step: 9, loss is 0.7372\n",
      "epoch: 1 step: 10, loss is 0.6948\n",
      "\n",
      "...\n",
      "epoch: 10 step: 380, loss is 0.3090\n",
      "epoch: 10 step: 381, loss is 0.2692\n",
      "epoch: 10 step: 382, loss is 0.3088\n",
      "epoch: 10 step: 383, loss is 0.2008\n",
      "epoch: 10 step: 384, loss is 0.1450\n",
      "epoch: 10 step: 385, loss is 0.2522\n",
      "epoch: 10 step: 386, loss is 0.2532\n",
      "epoch: 10 step: 387, loss is 0.3558\n",
      "epoch: 10 step: 388, loss is 0.2641\n",
      "epoch: 10 step: 389, loss is 0.2334\n",
      "epoch: 10 step: 390, loss is 0.1966\n",
      "Epoch time: 43320.815, per step time: 111.079, avg loss: 0.262\n",
      "************************************************************\n",
      "============== Training Success ==============\n"
     ]
    }
   ],
   "source": [
    "from mindspore import Model\n",
    "from mindspore.train.callback import CheckpointConfig, ModelCheckpoint, TimeMonitor, LossMonitor\n",
    "from mindspore.nn import Accuracy\n",
    "\n",
    "\n",
    "model = Model(network, loss, opt, {'acc': Accuracy()})\n",
    "loss_cb = LossMonitor()\n",
    "print(\"============== Starting Training ==============\")\n",
    "config_ck = CheckpointConfig(save_checkpoint_steps=cfg.save_checkpoint_steps,\n",
    "                             keep_checkpoint_max=cfg.keep_checkpoint_max)\n",
    "ckpoint_cb = ModelCheckpoint(prefix=\"lstm\", directory=args.ckpt_path, config=config_ck)\n",
    "time_cb = TimeMonitor(data_size=ds_train.get_dataset_size())\n",
    "if args.device_target == \"CPU\":\n",
    "    model.train(cfg.num_epochs, ds_train, callbacks=[time_cb, ckpoint_cb, loss_cb], dataset_sink_mode=False)\n",
    "else:\n",
    "    model.train(cfg.num_epochs, ds_train, callbacks=[time_cb, ckpoint_cb, loss_cb])\n",
    "print(\"============== Training Success ==============\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 模型验证\n",
    "\n",
    "创建并加载验证数据集（`ds_eval`），加载由**训练**保存的CheckPoint文件，进行验证，查看模型质量，此步骤用时约30秒。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "============== Starting Testing ==============\n",
      "============== {'acc': 0.8495592948717948} ==============\n"
     ]
    }
   ],
   "source": [
    "from mindspore.train.serialization import load_checkpoint, load_param_into_net\n",
    "\n",
    "\n",
    "args.ckpt_path = f'./lstm-{cfg.num_epochs}_390.ckpt'\n",
    "print(\"============== Starting Testing ==============\")\n",
    "ds_eval = lstm_create_dataset(args.preprocess_path, cfg.batch_size, training=False)\n",
    "param_dict = load_checkpoint(args.ckpt_path)\n",
    "load_param_into_net(network, param_dict)\n",
    "if args.device_target == \"CPU\":\n",
    "    acc = model.eval(ds_eval, dataset_sink_mode=False)\n",
    "else:\n",
    "    acc = model.eval(ds_eval)\n",
    "print(\"============== {} ==============\".format(acc))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 训练结果评价\n",
    "\n",
    "根据以上一段代码的输出可以看到，在经历了10轮epoch之后，使用验证的数据集，对文本的情感分析正确率在85%左右，达到一个基本满意的结果。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 总结\n",
    "\n",
    "以上便完成了MindSpore自然语言处理应用的体验，我们通过本次体验全面了解了如何使用MindSpore进行自然语言中处理情感分类问题，理解了如何通过定义和初始化基于LSTM的`SentimentNet`网络进行训练模型及验证正确率。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}